// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.intellij.plugins.markdown.ui.preview.jcef

import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.ui.jcef.JBCefBrowser
import com.intellij.ui.jcef.JBCefJSQuery

/**
 * Message passing interface for the JCEF browser preview.
 * See [BrowserPipe.js] for the browser side support code.
 */
class BrowserPipe(private val browser: JBCefBrowser) : Disposable {
  private val events = HashMap<String, JBCefJSQuery>()

  init {
    addBrowserEvents(WINDOW_READY_EVENT)
  }

  /**
   * Generate javascript code to register events.
   */
  fun inject(): String {
    return events.entries.fold(StringBuilder()) { builder, (tag, query) ->
      builder.append(
        // language=JavaScript
        """
        window.messagePipe.ideApi.subscribe("$tag", function(data) {
          ${query.inject("data")}
        });
        """.trimIndent()
      )
    }.append("window.addEventListener(\"load\", () => messagePipe.post(\"$WINDOW_READY_EVENT\"));").toString()
  }

  /**
   * Register events names. Any events added after execution of the browser-side code,
   * generated by [inject], will not be registered.
   */
  fun addBrowserEvents(vararg names: String) {
    names.forEach {
      events[it] = JBCefJSQuery.create(browser)
    }
  }

  /**
   * Post event [eventName] with [data] for the browser subscribers.
   *
   * Precondition: [eventName] should be registered with [addBrowserEvents] and
   * the code, generated by [inject], should be successfully executed in the browser.
   */
  fun post(eventName: String, data: String) {
    browser.cefBrowser.executeJavaScript(
      // language=JavaScript
      """
        window.messagePipe.ideApi.post("$eventName", $data);
      """.trimIndent(),
      null,
      0
    )
  }

  /**
   * Subscribe to [eventName], triggered from the browser-side code.
   *
   * Precondition: [eventName] should be registered with [addBrowserEvents].
   *
   * Events will not be triggered until the code, generated by [inject],
   * is not successfully executed in the browser.
   */
  fun subscribe(eventName: String, callback: (String) -> Unit) {
    val value = events[eventName]
                ?: error("Could not subscribe to unregistered event with tag: $eventName!")
    value.addHandler {
      callback(it)
      null
    }
  }

  override fun dispose() {
    events.values.forEach(Disposer::dispose)
  }

  companion object {
    const val WINDOW_READY_EVENT = "documentReady"
  }
}
